# -*- coding: utf-8 -*-

import os
import sys
from lp.settings import SAVEDIR, TMPDIR, GLPSOL

"""
Linear programming web interface
"""

class HelperDict(dict):
    def getlist(self, key):
        try:
            return super(HelperDict, self).__getitem__(key)
        except KeyError:
            return []

class ULP:
    """
    Uloha linearneho programovania
    """

    # Praca s formami:
    form_list = ('obj_func_val', 'obj_func_sign', 'const_l_val',
            'const_l_sign', 'const_r_val', 'const_r_sign')

    def __init__(self, project_data):
        """
        Nastavime zakladne data a zoznamy ULP 
        """
        self.proname = project_data.proname
        self.n = int(project_data.n)
        self.m = int(project_data.m)
        self.objname = project_data.objname
        self.objtype = str(project_data.objtype)
        self.nonzero = project_data.nonzero
        self.binary = project_data.binary
        self.integer = project_data.integer

        self.obj_func = []
        self.const_l = []
        self.const_r = []
        self.solver_options = []
        self.glpsol = GLPSOL
    
        self.data_was_changed = 0
        self.tmpdir = TMPDIR
        self.savedir = SAVEDIR
        self.modfile = 0
        self.solfile = 0
        self.bodfile = 0
        self.outfile = 0

    def set_all_default(self):
        """
        Nastavi vsetky data v ULP na 0, +, <=
        """
        for i in range(self.n):
            self.obj_func.append(['+', 0])
        for i in range(self.m):
            self.const_r.append(['<=', 0])
            _tmp = []
            for j in range(self.n):
                _tmp.append(['+', 0])
            self.const_l.append(_tmp)

    def check_if_number(self, str):
        try:
            _tmp = int(str)
        except:
            try:
                _tmp = float(str)
            except:
                raise ValueError

        return _tmp

    def flat_to_post(self, flat_dict):
        """
        Prehodime flat slovnik na POST slovnik
        """
        import re
        post_dict = HelperDict({ 'obj_func_sign' : [], 'obj_func_val': [], 'const_l_val': [], 
            'const_l_sign': [], 'const_r_val': [], 'const_r_sign': [] })

        def _sign_val(signval):
            _tmp = ["", "+", "0"]
            if signval != None:
                _tmp = re.split(r'(\+|\-)', signval)
            return _tmp[1], _tmp[2]

        def _cleanstr(str):
            return re.sub(r'\s', '', str)

        def _normalize(form):
            str = _cleanstr(form)
            if str[0] != '+' and str[0] != '-':
                str = '+' + str
            _tmp = re.split(r'(x\d+)', str)
            return _tmp[:-1]

        def _isolate(func, val, sign):
            added = []
            for i in range(1, self.n+1):
                xvar = "x" + str(i)

                found = False
                for j in range(0, len(func)):
                    if xvar == func[j]:
                        if xvar in added:
                            raise KeyError
                        fvalue = func[j-1]
                        try:
                            self.check_if_number(fvalue)
                        except:
                            if fvalue == "+" or fvalue == "-":
                                fvalue = fvalue + "1"
                            else:
                                raise ValueError
                        xsign, xval = _sign_val(fvalue)
                        found = True

                if not found:
                    xsign, xval = _sign_val(None)

                sign.append(xsign)
                val.append(xval)
                added.append(xvar)

        _isolate(_normalize(flat_dict['obj_func']), post_dict['obj_func_val'], post_dict['obj_func_sign'])
        
        a = 0
        for const in flat_dict['const_area'].split('\n'):
            if not const.isspace() and const != '':
                a = a + 1
                if a > self.m:
                    raise KeyError
                cc = re.split(r'(<=|>=|>|<|=)', const) #TODO |≤|≥
                _isolate(_normalize(cc[0]), post_dict['const_l_val'], post_dict['const_l_sign'])

                r_sign = cc[1]
                r_val = _cleanstr(cc[2])
                """
                if r_sign == u'≤': #BUG
                    r_sing = '<='
                if r_sing == u'≥':
                    r_sing = '>='
                """

                post_dict['const_r_sign'].append(r_sign)
                post_dict['const_r_val'].append(r_val)

        return post_dict

    def post_to_dict(self, post_dict):
        """
        POST request prehodime na zoznamy a vratime slovnik
        """
        form_dict = {}
        form_dict['n'] = self.n
        form_dict['m'] = self.m
        
        def check_post_list(list):
            for i in list:
                self.check_if_number(i)

        for item in ULP.form_list:
            post_list = post_dict.getlist(item)
            if item == 'obj_func_val' or item == 'const_l_val' or item == 'const_r_val':
                check_post_list(post_list)
            form_dict[item] = post_list

        return form_dict
    
    def dict_to_data(self, form_dict):
        """
        Haluze: z input forms spravime naspat normalne data
        """
        # S tymito premennymi sa bude lahsie pracovat
        _ofv = form_dict[ULP.form_list[0]]
        _ofs = form_dict[ULP.form_list[1]]
        _clv = form_dict[ULP.form_list[2]]
        _cls = form_dict[ULP.form_list[3]]
        _crv = form_dict[ULP.form_list[4]]
        _crs = form_dict[ULP.form_list[5]]
        _old_n = form_dict['n']
        _old_m = form_dict['m']

        # Zmena "n" sposobi zmeny v obj_func a const_l
        if self.n > _old_n:
            _ofv.extend((self.n - _old_n) * '0')
            _ofs.extend((self.n - _old_n) * '+')
            #for i in range(self.n - _old_n):
            #    _ofv.append('0')
            #    _ofs.append('+')
            _idx = range(_old_n, (_old_m * _old_n) + 1, _old_n)
            _idx.reverse()
            for i in _idx:
                for j in range(self.n - _old_n):
                    _clv.insert(i, '0')
                    _cls.insert(i, '+')
            self.data_was_changed = 1
        elif self.n < _old_n:
            del _ofv[self.n:]
            del _ofs[self.n:]
            #for i in range(_old_n - self.n):
            #    del _ofv[-1]
            #    del _ofs[-1]
            _idx = range(self.n, (_old_m * _old_n), _old_n)
            _idx.reverse()
            for i in _idx:
                for j in range(_old_n - self.n):
                    del _clv[i]
                    del _cls[i]
            self.data_was_changed = 1

        # Zmena "m" sposobi zmeny v const_r a const_l
        if self.m > _old_m:
            #_crv.extend((self.m - old_m) * '0')
            #_crs.extend((self.m - old_m) * '<=')
            for i in range(self.m - _old_m):
                _crv.append('0')
                _crs.append('<=')
                _clv.extend(self.n * '0')
                _cls.extend(self.n * '+')
            self.data_was_changed = 1
        elif self.m < _old_m:
            #del _crv[self.m:]
            #del _crs[self.m:]
            for i in range(_old_m - self.m):
                del _crv[-1]
                del _crs[-1]
                del _clv[-self.n:]
                del _cls[-self.n:]
            self.data_was_changed = 1

        # Prehodime data z input form na nase data v maticiach
        self.obj_func = []
        self.const_l = []
        self.const_r = []
        for i in range(self.n):
            self.obj_func.append([_ofs[i], _ofv[i]])
        for i in range(self.m):
            self.const_r.append([_crs[i], _crv[i]])
            _tmp = []
            for j in range(self.n):
                _idx = self.n * i + j
                _tmp.append([_cls[_idx], _clv[_idx]])
            self.const_l.append(_tmp)

    def set_solver_options(self, data_dict):
        _tmp = []
        for item in data_dict:
            _tmp.append(str(data_dict[item]))
        self.solver_options = _tmp

    def create_lp_problem(self):
        # Creating problem
        procid = os.getpid()
        self.modfile = os.path.join(self.tmpdir, "%d-lp.mod" % procid)
        self.solfile = os.path.join(self.tmpdir, "%d-lp.sol" % procid)
        self.bodfile = os.path.join(self.tmpdir, "%d-lp.bod" % procid)
        self.outfile = os.path.join(self.tmpdir, "%d-lp.out" % procid)

        # Write lp mod file
        f = open(self.modfile, 'w')

        # Title
        #f.write('/* '+self.proname+' - glpsol model file */\n\n')
        f.write('/* glpsol model file */\n\n')

        objtmp = []
        vars = []
        for i in range(self.n):
            vars.append('x' + str(i + 1))
            var = ['var', vars[i]]
            if self.integer:
                var.append('integer')
            if self.binary:
                var.append('binary')
            if self.nonzero:
                var.append('>= 0')
            # Variables
            f.write(' '.join(var) + ';\n')
            objtmp.append(self.obj_func[i][0])
            objtmp.append(self.obj_func[i][1])
            objtmp.append('*')
            objtmp.append(vars[i])

        objtype = self.objtype + 'imize'
        objname = self.objname.replace(' ','_')
        obj_func = ''.join(objtmp)
        # Objective
        f.write('\n'+objtype+'')
        f.write('\n'+objname+': '+obj_func+';\n')

        f.write('\nsubject to\n')

        for i in range(self.m):
            const = []
            const.extend('C' + str(i+1) + ': ')
            for j in range(self.n):
                const.extend(self.const_l[i][j])
                const.extend('*')
                const.extend(vars[j])
            const.extend(self.const_r[i])

            # Constraints
            f.write(''.join(const) + ';\n')

        f.write('\nend; \n\n/* eof */\n')
        f.close()

    def check_lp_problem(self):
        # Check problem
        proc = self.glpsol + ' --check -m ' + self.modfile + ' &> ' + self.outfile

        rc = os.system(proc)
        if rc == 127 or rc == 32512:
            raise OSError

        # Read the output file
        f = file(self.outfile, 'r')
        lastline = 'koniec'
        for line in f:
            lastline = line
        f.close()
        
        if str(lastline) == 'Model has been successfully generated\n':
            pass
        elif str(lastline) == 'Problem has no columns\n':
            raise StandardError
        else:
            raise SystemError

    def solve_lp_problem(self):
        # Solve problem
        out = ['&>', self.outfile]
        opts = [self.glpsol, '-m', self.modfile, '-o', self.solfile, '--bounds', self.bodfile] + self.solver_options + out
        proc = ' '.join(opts)

        #rc = os.spawnvp(os.P_WAIT, 'glpsol', opts)
        rc = os.system(proc)
        if rc == 127:
            raise OSError

        # Read a GLPK solution file
        f = file(self.solfile, 'r')
        f.readline()
        rows = int(f.readline().split()[1])
        cols = int(f.readline().split()[1])
        nonz = int(f.readline().split()[1])
        statusString = f.readline()[12:-1]
        glpkStatus = {
            "INTEGER OPTIMAL"       : 'Optimal (integer)',
            "OPTIMAL"           : 'Optimal',
            "INFEASIBLE (FINAL)"    : 'Infeasible (final)',
            "INTEGER UNDEFINED"     : 'Undefined',
            "UNBOUNDED"         : 'Unbounded',
            "UNDEFINED"         : 'Undefined',
            #NEW vvvvv
            "INTEGER NON-OPTIMAL"   : 'Non-optimal (integer)',
            "INTEGER EMPTY"     : 'Empty (integer)',
            "INFEASIBLE (INTERMEDIATE)" : 'Infeasible (intermediate)',
            "FEASIBLE"          : 'Feasible',
            "INTERIOR UNDEFINED"    : 'Undefined (interior)',
            "INTERIOR OPTIMAL"      : 'Optimal (interior)',
        }
        if statusString not in glpkStatus:
            raise KeyError

        status = glpkStatus[statusString]
        isInteger = statusString in ["INTEGER OPTIMAL","INTEGER UNDEFINED","INTEGER NON-OPTIMAL","INTEGER EMPTY"]
        isInterior = statusString in ["INTERIOR UNDEFINED", "INTERIOR OPTIMAL"]

        if isInteger:
            objective = int(f.readline().split()[3])
        else:
            objective = float(f.readline().split()[3])

        values = {}
        for i in range(3):
            f.readline()
        for i in range(rows):
            line = f.readline().split()
            if len(line) == 2:
                f.readline()
        for i in range(3):
            f.readline()
        for i in range(cols):
            line = f.readline().split()
            name = line[1]
            if len(line) == 2:
                line = [0,0]+f.readline().split()
            if isInteger or isInterior:
                if line[2] == "*":
                    value = int(line[3])
                else:
                    value = float(line[2])
            else:
                value = float(line[3])
            values[name] = value

        f.close()

        bound_obj = {}
        bound_const = {}
        bound_var = {} # TODO
        # Read a GLPK sensitive bounds file
        if os.access(self.bodfile, os.F_OK) and status == 'Optimal' and not isInterior:
            f = file(self.bodfile, 'r')
            for i in range(18):
                f.readline()
            for i in range(cols):
                line = f.readline().split()

                for x in range(len(line)):
                    if line[x] == 'eps':
                        eps = x
                        line[eps-1] = str(line[eps-1]) + 'eps'
                        del line[eps]

                name = line[1]
                value = line[-3:]
                bound_obj[name] = value
            for i in range(5):
                f.readline()
            for i in range(rows-1):
                line = f.readline().split()
                name = line[1]
                line = f.readline().split()
                value = line[-3:]
                bound_const[name] = value
            f.close()

        return {'status': status, 'values': values, 'objective': objective,
                'bound_obj': bound_obj, 'bound_const': bound_const}

    def get_file(self, file):
        _tmp = ""
        if os.access(file, os.F_OK):
            f = open(file, 'r')
            _tmp = f.read()
            f.close()
        return _tmp

    def clean_up(self, files=None):
        # Clean up temp. files
        if not files:
            try: os.remove(self.modfile)
            except: pass

            try: os.remove(self.solfile)
            except: pass

            try: os.remove(self.bodfile)
            except: pass

            try: os.remove(self.outfile)
            except: pass
        else:
            try:
                from datetime import datetime
                stamp = datetime.now().strftime("%Y_%m_%d-%H_%M_%S")
                savefile = os.path.join(self.savedir, "lp-error-%s.txt" % stamp)
                try:
                    fw = open(savefile, 'w')
                    for readfile in files:
                        try:
                            fr = open(readfile, 'r')
                            fw.write('\n' + readfile + '\n')
                            fw.write(fr.read())
                            fr.close()
                            os.remove(readfile)
                        except: pass
                    try: fw.write(self.solver_options)
                    except: pass
                finally:
                    fw.close()
            except:
                pass
